TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { NextResponse, type NextRequest } from 'next/server';2import { createReadStream } from 'node:fs';3import { stat } from 'node:fs/promises';4import { Readable } from 'node:stream';5import { getSql } from '@rareindex/database';6import { ensureOriginal, ensureVariant, findOriginal, imageKey, nearestWidth, negativeFor, variantPath, type OriginalInfo } from '@/lib/images-core';7import { verifyImageSignature } from '@/lib/images';89export const dynamic = 'force-dynamic';10export const runtime = 'nodejs';1112const IMMUTABLE = 'public, max-age=31536000, immutable';13const NEGATIVE = 'public, max-age=3600, stale-while-revalidate=600';1415/**16 * GET /img/<sha1>.<webp|avif>?w=<width>&u=<base64url(url)>&s=<sig>17 * Serves a cached, resized copy of a third-party product image. The original URL must be18 * HMAC-signed by the server (any page that renders it) or already known in the `images` table.19 */20export async function GET(req: NextRequest, ctx: { params: Promise<{ key: string }> }) {21 const { key: rawKey } = await ctx.params;22 const m = rawKey.match(/^([a-f0-9]{40})(?:\.(webp|avif))?$/);23 if (!m) return new NextResponse('Not found', { status: 404, headers: { 'cache-control': NEGATIVE } });24 const key = m[1]!;25 const fmt = (m[2] as 'webp' | 'avif' | undefined) ?? 'webp';26 const width = nearestWidth(req.nextUrl.searchParams.get('w'));2728 // Fast path: variant already on disk.29 try {30 const vpath = variantPath(key, width, fmt);31 const s = await stat(vpath);32 return fileResponse(vpath, s.size, `image/${fmt}`, req);33 } catch {34 /* build below */35 }3637 // Resolve the source URL: signed param first, then DB lookup by cache key / sha1(url).38 let url: string | null = null;39 const u = req.nextUrl.searchParams.get('u');40 const s = req.nextUrl.searchParams.get('s');41 if (u && s) {42 try {43 const decoded = Buffer.from(u, 'base64url').toString('utf8');44 if (imageKey(decoded) === key && verifyImageSignature(decoded, s)) url = decoded;45 } catch {46 url = null;47 }48 }49 if (!url) {50 try {51 const sql = getSql();52 const rows = (await sql`select url from images where cache_key = ${key} limit 1`) as Array<{ url: string }>;53 url = rows[0]?.url ?? null;54 } catch {55 url = null;56 }57 }5859 let info: OriginalInfo | null = await findOriginal(key);60 if (!info) {61 if (!url) return new NextResponse('Unknown image', { status: 404, headers: { 'cache-control': NEGATIVE } });62 if (negativeFor(url)) return new NextResponse('Image unavailable', { status: 404, headers: { 'cache-control': NEGATIVE, 'x-ri-image': 'negative-cache' } });63 const out = await ensureOriginal(url);64 if (!out.ok) {65 void recordFailure(key, url, out.status, out.reason);66 return new NextResponse('Image unavailable', { status: 404, headers: { 'cache-control': NEGATIVE, 'x-ri-image': out.status } });67 }68 info = out.info;69 }70 try {71 const vpath = await ensureVariant(info, width, fmt);72 const st = await stat(vpath);73 return fileResponse(vpath, st.size, `image/${fmt}`, req);74 } catch (err) {75 console.error('[img] variant failed', key, err instanceof Error ? err.message : err);76 return new NextResponse('Image processing failed', { status: 500, headers: { 'cache-control': 'no-store' } });77 }78}7980function fileResponse(filePath: string, size: number, contentType: string, req: NextRequest): NextResponse {81 const etag = `"${size}-${filePath.slice(-24).replace(/[^a-z0-9]/gi, '')}"`;82 if (req.headers.get('if-none-match') === etag) return new NextResponse(null, { status: 304, headers: { etag, 'cache-control': IMMUTABLE } });83 const stream = Readable.toWeb(createReadStream(filePath)) as unknown as ReadableStream;84 return new NextResponse(stream, {85 status: 200,86 headers: { 'content-type': contentType, 'content-length': String(size), 'cache-control': IMMUTABLE, etag, 'x-content-type-options': 'nosniff', 'accept-ch': 'DPR, Width' },87 });88}8990async function recordFailure(key: string, url: string, status: string, reason: string): Promise<void> {91 try {92 const sql = getSql();93 await sql`update images set status = ${status}, error = ${reason.slice(0, 200)}, checked_at = now(), cache_key = ${key} where url = ${url}`;94 } catch {95 /* best effort */96 }97}98